/*

 * Copyright (c) 2017-2018, FastDev 刘强 (fastdev@163.com).

 *

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *      http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package of.hotswapinject.core.plugin;

import of.hotswapinject.agent.JarFileHelper;
import of.hotswapinject.core.attach.IOnAttach;
import of.hotswapinject.core.common.Const;
import of.hotswapinject.core.common.Util;
import of.hotswapinject.core.search.ISearch;
import sun.misc.JarFilter;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class PluginInitalize {
    private Map<String, Set<String>> map = new HashMap<String, Set<String>>();
    private Map<String, List<Object>> objectMap = new HashMap<String, List<Object>>();

    private static PluginInitalize instance = new PluginInitalize();

    public static class PluginManager {
        public List<IMethodFilter> methodFilterList;
        public List<ISearch> searchList;
        public List<IOnAttach> onAttachList;
        public List<String> pluginPathList;
    }

    public static PluginInitalize getInstance() {
        return instance;
    }

    public synchronized PluginManager reloadPlugins(String filePath) throws Exception {
        map.clear();
        objectMap.clear();

        PluginManager pluginManager = new PluginManager();
        List<String> pluginPathList = new ArrayList<String>();
        pluginManager.pluginPathList = pluginPathList;
        String pluginPath = new File(filePath).getParentFile().getPath() + Const.FILESEPARATOR + "plugins"  + Const.FILESEPARATOR;
        String[] jarPaths = new File(pluginPath).list(new JarFilter());
        if(jarPaths == null || jarPaths.length == 0) {
            String errorMsg = "pluginPath have no plugin, path is:" + pluginPath;
            Util.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }
        for(String jarPath : jarPaths) {
            pluginPathList.add(pluginPath + jarPath);
        }
        for(String path : pluginPathList) {
            JarFile jarFile = new JarFile(path);
            scanJarFile(map, jarFile);
        }
        ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();

        try {
            PluginClassLoader cl = new PluginClassLoader(PluginInitalize.class.getClassLoader(), pluginPathList);
            pluginManager.methodFilterList = load(cl, IMethodFilter.class);
            pluginManager.searchList = load(cl, ISearch.class);
            pluginManager.onAttachList = load(cl, IOnAttach.class);
            for(IMethodFilter methodFilter : pluginManager.methodFilterList) {
                Util.trace("methodFilter:" + methodFilter + "," + methodFilter.getClass().toString());
            }

            for(ISearch search : pluginManager.searchList) {
                Util.trace("search:" + search + "," + search.getClass().toString());
            }

            for(IOnAttach oa : pluginManager.onAttachList) {
                Util.trace("onAttach:" + oa + "," + oa.getClass().toString());
            }

            return pluginManager;
        }
        finally {
            Thread.currentThread().setContextClassLoader(oldLoader);
        }
    }


    private <T> List<T> load(PluginClassLoader loader, Class<T> pluginClass) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        String key = pluginClass.getName();
        List<Object> objList = objectMap.get(key);
        if(objList != null) {
            return (List<T>)objList;
        }
        Set<String> nameConfigList = map.get(key);
        if(nameConfigList == null) {
            return Collections.emptyList();
        }
        objList = new ArrayList<Object>();
        for(String name : nameConfigList) {
            Class cl = loader.loadClass(name);
            if(Util.isAbstract(cl)) {
                Util.error("Can't create instance for abstract class:" + name);
                continue;
            }
            Object plugin = cl.getConstructor().newInstance();
            objList.add(plugin);
        }
        objectMap.put(key, objList);
        return (List<T>)objList;
    }

    private void scanJarFile(Map<String, Set<String>> map, JarFile jarFile) throws IOException {
        Enumeration entries = jarFile.entries();
        while(entries.hasMoreElements()) {
            JarEntry entry = (JarEntry)entries.nextElement();
            if(entry.getName().startsWith("META-INF/services/") && !entry.isDirectory()) {
                String content = JarFileHelper.getContent(jarFile, entry.getName());
                String[] lines = content.split(Const.NEWLINE);
                String className = entry.getName().substring("META-INF/services/".length());
                Set<String> list = map.get(className);
                if(list == null) {
                    list = new HashSet<String>();
                    map.put(className, list);
                }
                for(String line : lines) {
                    list.add(line);
                }
            }
        }
    }
}